/**
 * 创建人:  @author huangpeng    
 * 创建时间:  2023年12月11日 16:35
 * 项目名称:  lemon-blog-cloud
 * 文件名称:  RecommendServiceImpl
 * 文件描述:  @Description: 文章推荐service实现类
 * <p>
 * All rights Reserved, Designed By huangpeng 
 * @Copyright: 2021-2023
 *
 */
package com.lemon.blog.web.service.impl;

import com.lemon.blog.web.domain.BiUser;
import com.lemon.blog.web.domain.BlBlog;
import com.lemon.blog.web.domain.BlBlogOptions;
import com.lemon.blog.web.mapper.BiUserMapper;
import com.lemon.blog.web.mapper.BlBlogMapper;
import com.lemon.blog.web.mapper.BlBlogOptionsMapper;
import com.lemon.blog.web.service.RecommendService;
import com.lemon.common.core.constant.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 包名称：com.lemon.blog.web.service.impl
 * 类名称：RecommendServiceImpl
 * 类描述：文章推荐service实现类
 * 创建人：@author huangpeng
 * 创建时间：2023年12月11日 16:35
 */
@Service
public class RecommendServiceImpl implements RecommendService {

    @Autowired
    private BiUserMapper biUserMapper;

    @Autowired
    private BlBlogOptionsMapper blogOptionsMapper;

    @Autowired
    private BlBlogMapper blBlogMapper;

    @Override
    public List<BlBlog> queryRecommendByUser(Long userId) {
        // 1.将user表里面所有用户查出来
        // 2.遍历所有用户，将点赞表，收藏表里面该用户的记录中的博客id都找出来，放在一个Set
        // 每遍历完一个用户就存Map里面
        Map<String, Object> optionsMap = blogOptionsMapper.selectMyOptionsCountByUserId(userId);
        if (optionsMap == null || (
            Integer.parseInt(String.valueOf(optionsMap.get("likeCount"))) == 0
            && Integer.parseInt(String.valueOf(optionsMap.get("collectionCount"))) == 0
            && Integer.parseInt(String.valueOf(optionsMap.get("readCount"))) == 0)
        ) {
            return new ArrayList<>();
        }
        BiUser biUser = new BiUser();
        List<BiUser> users = biUserMapper.selectBiUserList(biUser);
        Map<Long, Set<Long>> userToBlogs = new HashMap<>();
        Map<Long, Integer> num = new HashMap<>();
        for(BiUser user:users) {
            //这个Set存放当前遍历到的用户所交互过的所有博客的id
            Set<Long> blogIds = new HashSet<>();

            //下面这个blogIdsFromUpvote是用户所有阅读过的博客id
            BlBlogOptions blBlogOptions = new BlBlogOptions();
            blBlogOptions.setUserId(user.getId());
            blBlogOptions.setDvRead(Constants.NUM_ONE);
            List<BlBlogOptions> blBlogOptionsListRead = blogOptionsMapper.selectBlBlogOptionsList(blBlogOptions);
            if (!blBlogOptionsListRead.isEmpty()) {
                List<Long> blogIdsFromLike = blBlogOptionsListRead.stream().map(BlBlogOptions::getBlogId).collect(Collectors.toList());
                blogIds.addAll(blogIdsFromLike);
            }
            blBlogOptions.setDvRead(null);

            //下面这个blogIdsFromUpvote是用户所有点赞过的博客id
            blBlogOptions.setDvLike(Constants.NUM_ONE);
            List<BlBlogOptions> blBlogOptionsListLike = blogOptionsMapper.selectBlBlogOptionsList(blBlogOptions);
            if (!blBlogOptionsListLike.isEmpty()) {
                List<Long> blogIdsFromLike = blBlogOptionsListLike.stream().map(BlBlogOptions::getBlogId).collect(Collectors.toList());
                blogIds.addAll(blogIdsFromLike);
            }
            blBlogOptions.setDvLike(null);

            //下面这个blogIdsFromCollect是用户所有收藏了的博客id
            blBlogOptions.setDvCollection(Constants.NUM_ONE);
            List<BlBlogOptions> blBlogOptionsListCollect  = blogOptionsMapper.selectBlBlogOptionsList(blBlogOptions);
            if (!blBlogOptionsListCollect.isEmpty()) {
                List<Long> blogIdsFromCollect = blBlogOptionsListCollect.stream().map(BlBlogOptions::getBlogId).collect(Collectors.toList());
                blogIds.addAll(blogIdsFromCollect);
            }
            userToBlogs.put(user.getId(), blogIds);
            num.put(user.getId(), blogIds.size());
        }

        // 获取推荐的文章
        return recommendForUser(getSimMatrix(getCFMatrix(getItemToUsers(userToBlogs)), num), userToBlogs, 10, 10, userId);
    }

    /**
     * 获取推荐的博客的id集合
     * @Title: recommendForUser
     * @Description: 获取推荐的博客的id集合
     * @param sim 每个用户所交互的博客总数求相似度-jaccard公式
     * @param itemToUser 点赞表，收藏表里面该用户的记录中的博客id都找出来，放在一个Set中作为value, 用户id为key
     * @param k 取前k个用户
     * @param n 取前n篇文章
     * @param targetUser 当前用户id
     * @return: List<BlBlog>
     * @throws:
     * @author: huangpeng
     * @Date: 2023/12/11 16:42
     */
    public List<BlBlog> recommendForUser(Map<Long, Map<Long, Double>> sim,
                                         Map<Long, Set<Long>> itemToUser,
                                         int k, int n, Long targetUser) {
        System.out.println("给测试用户进行推荐....");
        Map<Long, Double> itemRank = new HashMap<>();
        if (sim.get(targetUser) == null) {
            // 使用算法获取推荐文章id为空
            return new ArrayList<>();
        }
        if (itemToUser.containsKey(targetUser)) {
            // sim[u] 的格式为 {user_id: similarity,....}
            // 按照相似度进行排序，然后取前 k 个
            List<Map.Entry<Long, Double>> sortedSim = new ArrayList<>(sim.get(targetUser).entrySet());
            sortedSim.sort((o1, o2) -> Double.compare(o2.getValue(), o1.getValue()));
            System.out.println("检查对相似度矩阵排序后的矩阵");
            for (Map.Entry<Long, Double> entry : sortedSim) {
                Long item = entry.getKey();
                Double similarity = entry.getValue();
                System.out.println("用户 " + item + ", 相似度: " + similarity);
            }
            for (int i = 0; i < k; i++) {
                //前k个相似度高的用户
                if (i >= sortedSim.size())
                    break;

                Long similarUser = sortedSim.get(i).getKey();
                double score = sortedSim.get(i).getValue();
                // 找出相似用户中有交互的博客，但当前用户并未交互过的博客进行推荐
                for (Long item : itemToUser.get(similarUser)) {
                    //如果用户已经对该博客交互过，就不用再推荐
                    if (itemToUser.get(targetUser).contains(item)) {
                        continue;
                    }
                    //这里就得到的推荐候选的一个集合
                    itemRank.put(item, itemRank.getOrDefault(item, 0.0) + score);
                }
            }
        }

        // 根据评分进行排序，取排名靠前的 n 个博客作为推荐列表
        Set<Long> recommendedItems = getBlogIds(n, itemRank);
        return recommendedItems.isEmpty() ? new ArrayList<>() : blBlogMapper.selectByPrimaryKeyList(new ArrayList<>(recommendedItems));
    }

    /**
     * 根据评分进行排序，取排名靠前的 n 个博客作为推荐列表
     * @Title: getBlogIds
     * @Description: 根据评分进行排序，取排名靠前的 n 个博客作为推荐列表
     * @param n 取排名靠前的前n个
     * @param itemRank 用户所交互的（博客id，推荐相似度）
     * @return: Set<Long> 博客id列表
     * @throws:
     * @author: huangpeng
     * @Date: 2023/12/17 21:25
     */
    private static Set<Long> getBlogIds(int n, Map<Long, Double> itemRank) {
        List<Map.Entry<Long, Double>> topNItems = new ArrayList<>(itemRank.entrySet());
        topNItems.sort((o1, o2) -> Double.compare(o2.getValue(), o1.getValue()));
        Set<Long> recommendedItems = new HashSet<>();
        for (int i = 0; i < Math.min(n, topNItems.size()); i++) {
            recommendedItems.add(topNItems.get(i).getKey());
        }
        return recommendedItems;
    }

    /**
     * 获取某篇文章所对应的交互（点赞或收藏）过这篇文章的用户的集合
     * @Title: getItemToUsers
     * @Description: 获取某篇文章所对应的交互（点赞或收藏）过这篇文章的用户的集合
     * @param userToBlog
     * @return: Map<Long,Set<Long>>
     * @throws: 
     * @author: huangpeng
     * @Date: 2023/12/11 16:37
     */
    public Map<Long, Set<Long>> getItemToUsers(Map<Long, Set<Long>> userToBlog) {
        Map<Long, Set<Long>> itemToUsers = new HashMap<>();

        for (Map.Entry<Long, Set<Long>> entry : userToBlog.entrySet()) {
            Long userId = entry.getKey();
            Set<Long> blogs = entry.getValue();

            for (Long blogId : blogs) {
                //如果当前博客对应的用户集合中没有用户，就新建一个Set再把当前用户加进去，如果有的话就之间加进去
                Set<Long> users = itemToUsers.getOrDefault(blogId, new HashSet<>());
                users.add(userId);
                itemToUsers.put(blogId, users);
            }
        }

        return itemToUsers;
    }

    /**
     * 获取协同过滤矩阵是对于用户A，其他与A有共同交互过的博客的用户ID和共同交互过的博客的数量
     * @Title: getCFMatrix
     * @Description: 获取协同过滤矩阵是对于用户A，其他与A有共同交互过的博客的用户ID和共同交互过的博客的数量
     * @param itemToUsers
     * @return: Map<Long,Map<Long,Long>>
     * @throws:
     * @author: huangpeng
     * @Date: 2023/12/11 16:40
     */
    public Map<Long, Map<Long, Long>> getCFMatrix(Map<Long, Set<Long>> itemToUsers) {
        Map<Long, Map<Long, Long>> cfmatrix = new HashMap<>();

        System.out.println("开始构建协同过滤矩阵....");

        // 遍历所有的博客，统计用户两两之间交互的博客数
        for (Map.Entry<Long, Set<Long>> entry : itemToUsers.entrySet()) {
            Set<Long> users = entry.getValue();

            // 首先统计每个用户交互的博客个数
            for (Long u : users) {//遍历所有该博客对应的交互过的用户

                // 统计每个用户与其它用户共同交互的博客个数
                if (!cfmatrix.containsKey(u)) {
                    cfmatrix.put(u, new HashMap<>());
                }

                for (Long v : users) {//再次遍历所有用户，对不是u的其他用户进行操作
                    if (!v.equals(u)) {
                        if (!cfmatrix.get(u).containsKey(v)) {
                            cfmatrix.get(u).put(v, 0L);
                        }
                        cfmatrix.get(u).put(v, cfmatrix.get(u).get(v) + 1);
                    }
                }
            }
        }

        //还要返回num这个Map
        return cfmatrix;
    }

    /**
     * 根据协同过滤矩阵和每个用户所交互的博客总数求相似度-jaccard公式
     * @Title: getSimMatrix
     * @Description: 根据协同过滤矩阵和每个用户所交互的博客总数求相似度-jaccard公式
     * @param cfmatrix
     * @param num
     * @return: Map<Long, Map<Long,Double>>
     * @throws:
     * @author: huangpeng
     * @Date: 2023/12/11 16:42
     */
    public Map<Long, Map<Long, Double>> getSimMatrix(Map<Long, Map<Long, Long>> cfmatrix, Map<Long, Integer> num) {
        Map<Long, Map<Long, Double>> sim =new HashMap<>();
        System.out.println("构建用户相似度矩阵....");

        for (Map.Entry<Long, Map<Long, Long>> entry : cfmatrix.entrySet()) {//遍历协同过滤矩阵，遍历每个键值对
            Long u = entry.getKey();
            Map<Long, Long> otherUser = entry.getValue();

            for (Map.Entry<Long, Long> userScore : otherUser.entrySet()) {
                Long v = userScore.getKey();
                if(!sim.containsKey(u)) {
                    sim.put(u,new HashMap<>());
                }
                System.out.println(num.get(u) + ",aaaaaaa," + num.get(v));
                System.out.println(sim.get(u));
                System.out.println(cfmatrix.get(u).get(v));
                sim.get(u).put(v, cfmatrix.get(u).get(v) / Math.sqrt(num.get(u) * num.get(v)));
            }
        }

        return sim;
    }

}
